#pragma once

#include <bitset>

#include <cftc/typecontainer/list.h>
#include <cftc/typecontainer/unordered_map.h>

#include "map/griddefines.h"
#include "map/cell.h"
#include "map/gridmap.h"
#include "object/objectguid.h"
#include "dbc/structure.h"
#include "vmap/dynamic_map_tree.h"
#include "map/creature_linking_mgr.h"
#include "script/scriptmgr.h"

class WorldPacket;
class InstanceData;
class Group;
class MapPersistentState;
class WorldPersistentState;
class DungeonPersistentState;
class BattleGroundPersistentState;
class BattleGround;
class GameObjectModel;
class TerrainInfo;

namespace game::system
{
    class WeatherSystem;
}

namespace mt
{
    class CreatureInfo;
}

#define MIN_UNLOAD_DELAY      1                             // immediate unload

class Map
{
    friend class MapReference;
    friend class ObjectGridLoader;
    friend class ObjectWorldLoader;

protected:
    Map(unsigned int id, time_t, unsigned int InstanceId);

public:
    virtual ~Map();

    // currently unused for normal maps
    bool CanUnload(unsigned int diff)
    {
        if (!m_unloadTimer)
        {
            return false;
        }
        if (m_unloadTimer <= diff)
        {
            return true;
        }
        m_unloadTimer -= diff;
        return false;
    }

    virtual bool Add(battle::object::Player*);
    virtual void Remove(battle::object::Player*, bool);
    template<class T> void Add(T*);
    template<class T> void Remove(T*, bool);

    static void DeleteFromWorld(battle::object::Player* player);        // player object will deleted at call

    virtual void Update(const unsigned int&);

    void MessageBroadcast(battle::object::Player const*, WorldPacket*, bool to_self);
    void MessageBroadcast(battle::object::WorldObject const*, WorldPacket*);
    void MessageDistBroadcast(battle::object::Player const*, WorldPacket*, float dist, bool to_self, bool own_team_only = false);
    void MessageDistBroadcast(battle::object::WorldObject const*, WorldPacket*, float dist);

    float GetVisibilityDistance() const { return m_VisibleDistance; }
    // function for setting up visibility distance for maps on per-type/per-Id basis
    virtual void InitVisibilityDistance();

    void PlayerRelocation(battle::object::Player*, float x, float y, float z, float angl);
    void CreatureRelocation(battle::object::Creature* creature, float x, float y, float z, float orientation);

    template<class T, class CONTAINER> void Visit(const Cell& cell, TypeContainerVisitor<T, CONTAINER>& visitor);

    bool IsRemovalGrid(float x, float y) const
    {
        GridPair p = map::grid::ComputeGridPair(x, y);
        return (!getNGrid(p.x_coord, p.y_coord) || getNGrid(p.x_coord, p.y_coord)->GetGridState() == game::system::grid::GRID_STATE_REMOVAL);
    }
    bool IsLoaded(float x, float y) const
    {
        GridPair p = map::grid::ComputeGridPair(x, y);
        return loaded(p);
    }
    bool GetUnloadLock(const GridPair& p) const { return getNGrid(p.x_coord, p.y_coord)->getUnloadLock(); }
    void SetUnloadLock(const GridPair& p, bool on) { getNGrid(p.x_coord, p.y_coord)->setUnloadExplicitLock(on); }
    void ForceLoadGrid(float x, float y);
    bool UnloadGrid(const unsigned int& x, const unsigned int& y, bool pForce);
    virtual void UnloadAll(bool pForce);

    void ResetGridExpiry(NGridType& grid, float factor = 1) const
    {
        grid.ResetTimeTracker((time_t)((float)i_gridExpiry * factor));
    }

    time_t GetGridExpiry(void) const { return i_gridExpiry; }
    unsigned int GetId(void) const { return i_id; }

    // some calls like isInWater should not use vmaps due to processor power
    // can return INVALID_HEIGHT if under z+2 z coord not found height
    virtual void RemoveAllObjectsInRemoveList();

    bool CreatureRespawnRelocation(battle::object::Creature* c);        // used only in CreatureRelocation and ObjectGridUnloader

    bool CheckGridIntegrity(battle::object::Creature* c, bool moved) const;

    unsigned int GetInstanceId() const { return i_InstanceId; }
    virtual bool CanEnter(battle::object::Player* player);
    const char* GetMapName() const;

    bool Instanceable() const { return i_mapEntry && i_mapEntry->Instanceable(); }
    bool IsDungeon() const { return i_mapEntry && i_mapEntry->IsDungeon(); }
    bool IsRaid() const { return i_mapEntry && i_mapEntry->IsRaid(); }
    bool IsBattleGround() const { return i_mapEntry && i_mapEntry->IsBattleGround(); }
    bool IsContinent() const { return i_mapEntry && i_mapEntry->IsContinent(); }

    // can't be nullptr for loaded map
    MapPersistentState* GetPersistentState() const { return m_persistentState; }

    void AddObjectToRemoveList(battle::object::WorldObject* obj);

    void UpdateObjectVisibility(battle::object::WorldObject* obj, Cell cell, CellPair cellpair);

    void resetMarkedCells() { marked_cells.reset(); }
    bool isCellMarked(unsigned int pCellId) { return marked_cells.test(pCellId); }
    void markCell(unsigned int pCellId) { marked_cells.set(pCellId); }

    bool HavePlayers() const { return !m_mapRefManager.empty<battle::object::Player*>(); }
    unsigned int GetPlayersCountExceptGMs() const;
    bool ActiveObjectsNearGrid(unsigned int x, unsigned int y) const;

    /// Send a Packet to all players on a map
    void SendToPlayers(WorldPacket const* data) const;
    /// Send a Packet to all players in a zone. Return false if no player found
    bool SendToPlayersInZone(WorldPacket const* data, unsigned int zoneId) const;

    auto const& GetPlayers() const { return m_mapRefManager; }

    // per-map script storage
    enum ScriptExecutionParam
    {
        SCRIPT_EXEC_PARAM_NONE = 0x00,   // Start regardless if already started
        SCRIPT_EXEC_PARAM_UNIQUE_BY_SOURCE = 0x01,   // Start Script only if not yet started (uniqueness identified by id and source)
        SCRIPT_EXEC_PARAM_UNIQUE_BY_TARGET = 0x02,   // Start Script only if not yet started (uniqueness identified by id and target)
        SCRIPT_EXEC_PARAM_UNIQUE_BY_SOURCE_TARGET = 0x03,   // Start Script only if not yet started (uniqueness identified by id, source and target)
    };
    bool ScriptsStart(game::script::DBScriptType type, unsigned int id, battle::object::Object* source, battle::object::Object* target, ScriptExecutionParam execParams = SCRIPT_EXEC_PARAM_NONE);
    void ScriptCommandStart(game::script::ScriptInfo const& script, unsigned int delay, battle::object::Object* source, battle::object::Object* target);

    // must called with AddToWorld
    void AddToActive(battle::object::WorldObject* obj);
    // must called with RemoveFromWorld
    void RemoveFromActive(battle::object::WorldObject* obj);

    battle::object::Player* GetPlayer(ObjectGuid guid);
    battle::object::Creature* GetCreature(ObjectGuid guid);
    battle::object::Pet* GetPet(ObjectGuid guid);
    battle::object::Creature* GetAnyTypeCreature(ObjectGuid guid);      // normal creature or pet
    battle::object::GameObject* GetGameObject(ObjectGuid guid);
    battle::object::DynamicObject* GetDynamicObject(ObjectGuid guid);
    battle::object::Corpse* GetCorpse(ObjectGuid guid);                 // !!! find corpse can be not in world
    battle::object::Unit* GetUnit(ObjectGuid guid);                     // only use if sure that need objects at current map, specially for player case
    battle::object::WorldObject* GetWorldObject(ObjectGuid guid);       // only use if sure that need objects at current map, specially for player case

    using MapStoredObjectTypesContainer = cftc::typecontainer::UnorderedMap<
        ObjectGuid, 
        battle::object::Creature*, 
        battle::object::Pet*, 
        battle::object::GameObject*, 
        battle::object::DynamicObject*>;
    MapStoredObjectTypesContainer& GetObjectsStore() { return m_objectsStore; }
    void AddUpdateObject(battle::object::Object* obj)
    {
        i_objectsToClientUpdate.insert(obj);
    }

    void RemoveUpdateObject(battle::object::Object* obj)
    {
        i_objectsToClientUpdate.erase(obj);
    }

    // DynObjects currently
    unsigned int GenerateLocalLowGuid(HighGuid guidhigh);

    // get corresponding TerrainData object for this particular map
    const TerrainInfo* GetTerrain() const { return m_TerrainData; }

    void CreateInstanceData(bool load);
    InstanceData* GetInstanceData() const { return i_data; }
    virtual unsigned int GetScriptId() const { return game::script::ScriptMgr::Instance().GetBoundScriptId(game::script::SCRIPTED_MAP, GetId()); }

    void MonsterYellToMap(ObjectGuid guid, int textId, Language language, battle::object::Unit const* target) const;
    void MonsterYellToMap(mt::CreatureInfo const* cinfo, int textId, Language language, battle::object::Unit const* target, unsigned int senderLowGuid = 0) const;

    void PlayDirectSoundToMap(unsigned int soundId, unsigned int zoneId = 0) const;

    // Dynamic VMaps
    float GetHeight(float x, float y, float z) const;
    bool GetHeightInRange(float x, float y, float& z, float maxSearchDist = 4.0f) const;
    bool IsInLineOfSight(float x1, float y1, float z1, float x2, float y2, float z2) const;
    bool GetHitPosition(float srcX, float srcY, float srcZ, float& destX, float& destY, float& destZ, float modifyDist) const;

    // Object Model insertion/remove/test for dynamic vmaps use
    void InsertGameObjectModel(const GameObjectModel& mdl);
    void RemoveGameObjectModel(const GameObjectModel& mdl);
    bool ContainsGameObjectModel(const GameObjectModel& mdl) const;

    // Get Holder for Creature Linking
    CreatureLinkingHolder* GetCreatureLinkingHolder() { return &m_creatureLinkingHolder; }

    // Teleport all players in that map to choosed location
    void TeleportAllPlayersTo(TeleportLocation loc);

    // WeatherSystem
    game::system::WeatherSystem* GetWeatherSystem() const { return m_weatherSystem; }
    /** Set the weather in a zone on this map
     * @param zoneId set the weather for which zone
     * @param type What weather to set
     * @param grade how strong the weather should be
     * @param permanently set the weather permanently?
     */
    void SetWeather(unsigned int zoneId, WeatherType type, float grade, bool permanently);

    // Random on map generation
    bool GetReachableRandomPosition(battle::object::Unit* unit, float& x, float& y, float& z, float radius);
    bool GetReachableRandomPointOnGround(float& x, float& y, float& z, float radius);
    bool GetRandomPointInTheAir(float& x, float& y, float& z, float radius);
    bool GetRandomPointUnderWater(float& x, float& y, float& z, float radius, GridMapLiquidData& liquid_status);

    void LoadLocalTransports();

private:
    void LoadMapAndVMap(int gx, int gy);

    void SetTimer(unsigned int t) { i_gridExpiry = t < MIN_GRID_DELAY ? MIN_GRID_DELAY : t; }

    void SendInitSelf(battle::object::Player* player);

    void SendInitTransports(battle::object::Player* player);
    void SendRemoveTransports(battle::object::Player* player);

    bool CreatureCellRelocation(battle::object::Creature* creature, const Cell& new_cell);

    bool loaded(const GridPair&) const;
    void EnsureGridCreated(const GridPair&);
    bool EnsureGridLoaded(Cell const&);
    void EnsureGridLoadedAtEnter(Cell const&, battle::object::Player* player = nullptr);

    void buildNGridLinkage(NGridType* pNGridType) 
    { 
        grid_list_.add_tail(pNGridType->map_entry_.get());
    }

    NGridType* getNGrid(unsigned int x, unsigned int y) const
    {
        CFT_ASSERT(x < MAX_NUMBER_OF_GRIDS);
        CFT_ASSERT(y < MAX_NUMBER_OF_GRIDS);
        return i_grids[x][y];
    }

    bool isGridObjectDataLoaded(unsigned int x, unsigned int y) const { return getNGrid(x, y)->isGridObjectDataLoaded(); }
    void setGridObjectDataLoaded(bool pLoaded, unsigned int x, unsigned int y) { getNGrid(x, y)->setGridObjectDataLoaded(pLoaded); }
    void setNGrid(NGridType* grid, unsigned int x, unsigned int y);
    void ScriptsProcess();

    void SendObjectUpdates();
    std::set<battle::object::Object*> i_objectsToClientUpdate;
protected:
    const dbc::MapEntry* i_mapEntry = nullptr;
    unsigned int i_id = 0;
    unsigned int i_InstanceId = 0;
    unsigned int m_unloadTimer = 0;
    float m_VisibleDistance = 0.0f;
    MapPersistentState* m_persistentState = nullptr;

    cftc::typecontainer::List<battle::object::Player*> m_mapRefManager;

    typedef std::set<battle::object::WorldObject*> ActiveNonPlayers;
    ActiveNonPlayers m_activeNonPlayers;
    ActiveNonPlayers::iterator m_activeNonPlayersIter;
    MapStoredObjectTypesContainer m_objectsStore;

private:
    time_t i_gridExpiry = 0;

    NGridType* i_grids[MAX_NUMBER_OF_GRIDS][MAX_NUMBER_OF_GRIDS] = {nullptr};

    // Shared geodata object with map coord info...
    TerrainInfo* const m_TerrainData;
    bool m_bLoadedGrids[MAX_NUMBER_OF_GRIDS][MAX_NUMBER_OF_GRIDS];

    std::bitset<TOTAL_NUMBER_OF_CELLS_PER_MAP* TOTAL_NUMBER_OF_CELLS_PER_MAP> marked_cells;

    std::set<battle::object::WorldObject*> i_objectsToRemove;
    std::set<battle::object::Transport*> i_transports;

    typedef std::multimap<time_t, game::script::ScriptAction> ScriptScheduleMap;
    ScriptScheduleMap m_scriptSchedule;

    InstanceData* i_data = nullptr;
    cftc::typecontainer::List<NGridType*> grid_list_;

    // Map local low guid counters
    ObjectGuidGenerator<HIGHGUID_UNIT> m_CreatureGuids;
    ObjectGuidGenerator<HIGHGUID_GAMEOBJECT> m_GameObjectGuids;
    ObjectGuidGenerator<HIGHGUID_DYNAMICOBJECT> m_DynObjectGuids;
    ObjectGuidGenerator<HIGHGUID_PET> m_PetGuids;

    // Type specific code for add/remove to/from grid
    template<class T>
    void AddToGrid(T*, NGridType*, Cell const&);

    template<class T>
    void RemoveFromGrid(T*, NGridType*, Cell const&);
    // Holder for information about linked mobs
    CreatureLinkingHolder m_creatureLinkingHolder;
    // Dynamic Map tree object
    DynamicMapTree m_dyn_tree;
    // WeatherSystem
    game::system::WeatherSystem* m_weatherSystem;
};